package com.hmdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.dto.ScrollResult;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.Follow;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.hmdp.service.IFollowService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 服务实现类
 *      -- 点赞功能实现：
 *          -- 首先业务需求是一个人只能点赞一次 再次点击的时候是取消点赞
 *          -- 而且为了实现点赞后点赞按钮高亮显示 对应后端的话只用维护 isLike 属性
 *          -- 实现步骤：
 *              -- 给数据库添加一个 isLike 字段 标识用户是否点赞  缺点就是太重了
 *              -- 用 Redis 实现：用户点赞了的话就把用户添加到 set 集合中 通过 exist 方法判断用户是否点赞过
 *      -- Feed 流：
 *          -- 为什么 要实现关注用户的功能 ??? 因为要实现推送功能
 *          -- TimeLine ：
 *              -- 这个就是根据时间来推 常用于朋友圈
 *              -- 实现方案：
 *                  -- 拉模式(读扩散)：
 *                      -- 比如现在有三个 up 主 他们要发消息 首先为他们三个人每人准备一个发件箱 他们发消息的时候只用把
 *                          消息发送到发件箱中 而且带一个时间戳用于排序
 *                      -- 用户一般会配置一个收件箱 一般是空的 只有他在要收消息的时候 他就会把他关注的人的所有
 *                          发件箱里面一个个的去拉消息 拉到收件箱中
 *                      -- 特点：
 *                          -- 消息只有一份 只有在读的时候才会一个个的拉 每次读消息的时候都必须先把所有消息拉到收件箱中 然后再排序
 *                          -- 缺点就是读的很慢 如果有人关注很多 up 主的话会慢
 *                  -- 推模式(写扩散)：
 *                      -- 这个发件人没有发件箱 收件人会有一个收件箱 发件人发微博的时候会把这些东西复制好多份
 *                          投送到他每一个粉丝的收件箱里 然后对收件箱进行排序
 *                      -- 特点：
 *                          -- 发的慢
 *                  -- 推拉结合：
 *                      -- 普通用户用推模式
 *                      -- 大 V 粉丝多 但是有活跃粉和僵尸粉 后台先把粉丝分成两个部分 活跃粉丝用推模式(因为你天天看 所以要降低你看的延迟) 僵尸粉用拉模式(因为你看得少)
 *                  -- 本项目用的是推模式(没有发件箱)
 *          -- 智能算法 ：
 *              -- 比如抖音这个推荐算法
 *          -- 实现思路：
 *              -- 这里面没有发件箱 只有收件箱 当用户发布新的探店笔记时 要把探店笔记的内容复制发送给每一个粉丝
 *              -- 因为发布探店笔记的时候会把内容保存到数据库 所以推送的时候只用推送一个博客的id即可 用户到时候自己查内容
 *              -- 收件箱用 Redis 实现 而且要根据时间戳排序 list zset
 *              -- Feed 流的分页模式：
 *                  -- Feed 流中的数据会不断更新 所以数据的角标也在不断变化 因此不能采用传统的分页模式
 *                  -- 比如查完第一页数据 准备查第二页数据的时候 插入了一条新的数据在最前面 于是第一页的数据被往后
 *                      顶了一格 于是第二页数据优惠查到第一页最后一个数据
 *                  -- 滚动分页：
 *                      -- 每次记录查询的最后一条数据 下次从这个位置开始查
 *                      -- 显然 list 不支持 因为他只能按照角标查
 *                      -- sortedSet 可以根据时间戳排序 每次查询记录最小的那个时间戳 下次查询找比这个时间戳更小的
 *                      -- zrevrangebyscore z1 5 0 withscores limit 1 3
 *                      -- 显然一共有 4 个参数 分数的最大值和最小值 起始下标 查询数量
 *                      -- 其中 最小值固定为 0 每页查几条也是固定的 起始下标固定为 1 因为如果为 0 的话会把上次查询的最小值也包括进来(第一次给 0 后面给 1)
 *                          -- offset 就是表示要跳过的元素 如果有两个分数一样那跳一个就不够了
 *                              -- 规律：第一次不跳过 offset = 0 后面的跳过几个取决于上一次的查询的最小值中有几个跟他一样
 *                      -- 唯一变化的就是 max
 *                          -- 第一次来要取一个最大的 (当前时间戳就够了) 后面查的时候等于上一次查询的最小值
 *
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    private final IUserService userService ;
    private final IFollowService followService ;
    private final StringRedisTemplate stringRedisTemplate ;

    /**
     * 查询热门博客
     *
     * @param current
     * @return
     */
    @Override
    public List<Blog> queryHotBlog(Integer current) {
        // 根据用户查询
        Page<Blog> page = query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页数据
        List<Blog> records = page.getRecords();
        // 查询用户
        records.forEach(blog ->{
            // 查询用户
            queryBlogUser(blog);
            // 查看用户是点赞
            isBlogLiked(blog);
        });

        return records;
    }

    /**
     * 点赞博客
     *
     * @param id
     */
    @Override
    public void likeBlog(Long id) {
        // 获取登录的用户
        Long userId = UserHolder.getUser().getId();
        // 判断当前用户是否点赞过
        String key = RedisConstants.BLOG_LIKED_KEY + id ;   // 键值是跟当前博客关联的 即当前博客在 Redis 中维护一个集合 这个几个存了所有点赞用户
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());  // 这里用 score 代替 isMember
        // 判断当前用户的 userId 在不在这个集合里面
        if (score == null) {
             // 未点赞 可以点赞
            log.info("用户点赞！userId：{}，blogId：{}", userId, id) ;
             // 数据库点赞数量 + 1
            boolean isSuccess = this.update()
                    .setSql("liked = liked + 1")
                    .eq("id", id)
                    .update();
            if (!isSuccess) {
                throw new RuntimeException("点赞失败！") ;
            }
            // 保存用户到 Redis 的 set 集合
            stringRedisTemplate.opsForZSet().add(key, userId.toString(),System.currentTimeMillis()) ;   // 分数设置成时间戳是为了后面用来排序
         } else {
            // 如果已经点赞 取消点赞
            log.info("用户取消点赞！userId：{}，blogId：{}", userId, id) ;
            // 数据库点赞数 - 1
            boolean isSuccess = this.update()
                    .setSql("liked = liked - 1")
                    .eq("id", id)
                    .update();
            if (!isSuccess) {
                throw new RuntimeException("取消点赞失败！") ;
            }
            // 用户从 Redis 的 set 集合中移除
            stringRedisTemplate.opsForZSet().remove(key, userId.toString()) ;
        }

    }

    /**
     * 查询博客点赞用户排行榜
     *
     * @param id
     * @return
     */
    @Override
    public List<UserDTO> queryBlogLikes(Long id) {
        // 查询点赞 top5 排行榜    根据最新时间的 top
        String key = RedisConstants.BLOG_LIKED_KEY + id ;
        // 解析出其中的用户 userId
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if (top5 == null || top5.isEmpty()) {
            return Collections.emptyList() ;
        }
        List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList()); // 将 set 改成 List
        String idStr = StrUtil.join(",",ids);
        List<User> userList = userService.query()
                .in("id",ids)
                .last("ORDER BY FIELD(id," + idStr + ")")
                .list() ;
        List<UserDTO> userDTOList = userList.stream()
                .map(user -> {
                    return BeanUtil.copyProperties(user, UserDTO.class);
                })
                .collect(Collectors.toList());
        // 返回
        return userDTOList ;
    }

    /**
     * 保存博客
     *
     * @param blog
     * @return
     */
    @Override
    public Long saveBlog(Blog blog) {
        // 获取登录用户
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            throw new RuntimeException("用户未登录！") ;
        }
        blog.setUserId(user.getId());
        // 保存探店博文
        boolean isSuccess = save(blog);
        if (!isSuccess) {
            throw new RuntimeException("博客发布失败！") ;
        }
        // 推送部分
        // 查询笔记作者的所有粉丝
        List<Follow> follows = followService.query()
                .eq("follow_user_id", user.getId())
                .list();
        // 推送笔记 id 到所有粉丝
        follows.forEach(follow -> {
            // 获取粉丝 id
            Long userId = follow.getUserId();
            // 推送
            String key = RedisConstants.FEED_KEY + userId ; // 每个用户都有一个 zset
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(),System.currentTimeMillis()) ;
        });
        // 返回id
        return blog.getId();
    }

    /**
     * 分页查询关注用户博客
     *
     * @param max
     * @param offset
     */
    @Override
    public ScrollResult queryBlogOfFollow(Long max, Integer offset) {
        // 获取当前用户
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            throw new RuntimeException("用户未登录！") ;
        }
        Long userId = user.getId();
        // 查询收件箱
        String key = RedisConstants.FEED_KEY + userId ;
        // 滚动分页查询 这里面这个 set 是有序的 而且是是倒序
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
        // 非空判断
        if (typedTuples == null || typedTuples.isEmpty()) {
            throw new RuntimeException("没有更多数据！") ;
        }
        // id 结果集
        List<Long> ids = new ArrayList<>(typedTuples.size()) ;
        long minTime = 0 ;
        int os = 1 ;    // 每多找一个就 + 1
        // 解析数据 blogId minTime offset
        for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {// 获取 id
            String idStr = tuple.getValue();
            ids.add(Long.valueOf(idStr));
            // 获取分数
            // 这个 minTime 就是结果集中最小的时间戳 这个 set 集合本来就有序 只用获得最后一个 minTime 就行
            long time = tuple.getScore().longValue();
            if (time == minTime) {
                os++ ;
            } else {
                // 后取到的一定比前面的小 不一样说明当前这个 minTime 不是最小的
                minTime = time ;
                os = 1 ;    // 重置计数器
            }
        }
        // 查询数据
        // List<Blog> blogs = this.listByIds(ids); 这个是用 in 查的 不能保证有序
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = this.query()
                .in("id", ids)
                .last("ORDER BY FIELD(id," + idStr + ")")
                .list();
        // 给 blog 添加数据
        blogs.forEach(blog -> {
            queryBlogUser(blog) ;
            isBlogLiked(blog) ;
        });
        // 封装并返回数据
        ScrollResult scrollResult = ScrollResult.builder()
                .minTime(minTime)
                .offset(os)
                .list(blogs)
                .build();
        return scrollResult;
    }

    /**
     * 根据 id 查询博客
     *
     * @param id
     * @return
     */
    @Override
    public Blog queryBlogById(Long id) {
        // 查询 blog 信息
        Blog blog = this.getById(id) ;
        if (blog == null) {
            throw new RuntimeException("笔记不存在！") ;
        }
        // 查询 blog 有关的用户
        queryBlogUser(blog) ;   // 这里面直接传引用过去 可以在函数里面直接修改
        // 查询 blog 是否被点赞过
        isBlogLiked(blog) ;
        // 返回
        return blog ;
    }

    /**
     * 填充 blog 里面的用户信息
     * @param blog
     */
    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId) ;
        blog.setIcon(user.getIcon()) ;
        blog.setName(user.getNickName()) ;
    }

    /**
     * 查询用户是否被点赞
     */
    public void isBlogLiked(Blog blog) {
        // 获取登录用户
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            throw new RuntimeException("用户未登录！") ;
        }
        Long userId = user.getId();
        // 判断当前登录用户是否已经点赞
        String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        blog.setIsLike(score != null);
    }
}
